package com.example.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.*;

/**
 * @Author 刘武贵
 * @Date 2024-03-05 22:33
 * @Description 功能描述：网关配置类
 * 基于Sentinel 的Gateway限流是通过其提供的Filter来完成的，使用时只需注入对应的SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可。
 */
@Configuration
public class GatewayConfig {

    /**
     * 视图解析器列表，用于解析和返回相应的视图
     */
    private final List<ViewResolver> viewResolvers;

    /**
     * 服务器编解码配置器，用于配置服务器的编码和解码
     */
    private final ServerCodecConfigurer serverCodecConfigurer;

    /**
     * 通过注入的方式获取配置属性，这里配置的是发现服务路由ID的前缀
     */
    @Value("${spring.cloud.gateway.discovery.locator.route-id-prefix}")
    private String routeIdPrefix;

    /**
     * GatewayConfig 构造函数
     * 用于初始化网关的配置，包括视图解析器和服务器编解码器配置。
     *
     * @param viewResolversProvider 视图解析器的提供者，用于解析视图。如果提供者为空，则默认使用空列表。
     * @param serverCodecConfigurer 服务器编解码器配置，用于配置服务器的编解码器。
     */
    public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                         ServerCodecConfigurer serverCodecConfigurer) {
        // 获取视图解析器提供者提供的视图解析器列表，如果为空，则使用空列表
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        // 设置服务器编解码器配置
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 初始化一个限流的过滤器
     * 配置并返回一个全局过滤器，用于在网关中引入Sentinel作为流量控制工具。
     *
     * @return SentinelGatewayFilter 实例，该实例是一个全局过滤器，用于在Spring Cloud Gateway中应用Sentinel的流量控制。
     */
    @Bean
    /**
     * 设置过滤器的执行顺序为最高优先级
     */
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * 该方法在类的构造完成后执行，用于初始化网关规则、阻断处理器和自定义API。
     * 该方法没有参数，也没有返回值。
     */
    @PostConstruct
    public void init() {
        // 网关整合Sentinel限流-实现route维度限流
//       this.initGatewayRules();
        // 初始化阻断处理器
        this.initBlockHandlers();
        // 初始化自定义API 网关整合Sentinel限流-实现自定义API分组维度限流
        this.initCustomizedApis();
    }

    /**
     * 用于网关整合Sentinel限流-实现自定义API分组维度限流
     * 初始化自定义的API定义。
     * 该方法用于配置两个API的定义信息，包括API的名称和匹配规则。
     * 这两个API分别匹配不同的URL路径。
     */
    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        // 定义第一个API，匹配以"/server-user/user/api1"开头的任何请求
        ApiDefinition api1 = new ApiDefinition("user_api1")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    // 以/server-user/user/api1 开头的请求
                    add(new ApiPathPredicateItem().setPattern("/server-user/user/api1/**").
                            setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                }});

        // 定义第二个API，仅匹配URL路径"/server-user/user/api2/demo1"
        ApiDefinition api2 = new ApiDefinition("user_api2")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    // 以/server-user/user/api2/demo1 完成的url路径匹配
                    add(new ApiPathPredicateItem().setPattern("/server-user/user/api2/demo1"));
                }});
        definitions.add(api1);
        definitions.add(api2);
        // 将定义的API集合加载到Gateway的API定义管理器中
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }

    /**
     * 配置初始化的限流参数，基于Route模式 用于网关整合Sentinel限流-实现route维度限流
     * 此方法用于在应用启动时，自动加载针对SpringCloud Gateway的流控规则。
     * Sentinel整合SpringCloud Gateway时，会根据配置的route ID前缀和目标微服务名称生成API名称，
     * 从而实现对通过Gateway访问的微服务进行流量控制。
     * 通过预先配置好的规则，可以实现启动项目即可达到访问链接限流的效果，避免了手动在Sentinel管理界面配置规则的步骤。
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();

        /**
         * Sentinel整合SpringCloud Gateway使用的API类型为Route ID类型时，
         * 由于Sentinel为SpringCloud Gateway网关生成的API名称规则如下：
         * 生成的规则为：${spring.cloud.gateway.discovery.locator.route-id-prefix}后面直接加上目标微服务的名称，如下所示。
         * ${spring.cloud.gateway.discovery.locator.route-id-prefix}目标微服务的名称
         * 其中，${spring.cloud.gateway.discovery.locator.route-id-prefix}是在yml文件中配置的访问前缀
         *
         * 为了让通过服务网关访问目标微服务链接后，请求链路中生成的API名称与流控规则中生成的API名称一致，以达到启动项目即可实现访问链接的限流效果，
         * 而无需登录Setinel管理界面手动配置限流规则，可以将
         * 生成GatewayFlowRule对象的resource参数设置为${spring.cloud.gateway.discovery.locator.route-id-prefix}目标微服务的名称
         *
         * 当然，如果不按照上述配置，也可以在项目启动后，通过服务网关访问目标微服务链接后，在Sentinel管理界面的请求链路中找到对应的API名称所代表的请求链路，
         * 然后手动配置限流规则。
         **/
//        // 用户微服务网关
//        rules.add(this.getGatewayFlowRule("user-gateway"));
//        // 商品微服务网关
//        rules.add(this.getGatewayFlowRule("product-gateway"));
//        // 订单微服务网关
//        rules.add(this.getGatewayFlowRule("order-gateway"));
        // 根据目标微服务名称生成GatewayFlowRule对象，并加入到规则集合中
        // 这里使用了getResource方法动态获取目标微服务的名称，以实现更灵活的配置
        // user-gateway, product-gateway, order-gateway 分别代表用户微服务、商品微服务和订单微服务的网关规则
        // 用户微服务网关
        rules.add(this.getGatewayFlowRule(getResource("server-user")));
        // 商品微服务网关
        rules.add(this.getGatewayFlowRule(getResource("server-product")));
        // 订单微服务网关
        rules.add(this.getGatewayFlowRule(getResource("server-order")));
        // 加载规则集合到Sentinel
        GatewayRuleManager.loadRules(rules);
    }

    /**
     * 获取资源的ID，通过将路由ID前缀与目标服务名称连接起来生成。
     *
     * @param targetServiceName 目标服务的名称。这是一个必传参数，表示需要获取资源ID的服务名称。
     * @return 返回拼接后的资源ID字符串。如果路由ID前缀为空，则只返回目标服务名称。
     */
    private String getResource(String targetServiceName){
        // 如果路由ID前缀未初始化，则默认为空字符串
        if (routeIdPrefix == null){
            routeIdPrefix = "";
        }
        // 将路由ID前缀与目标服务名称连接并返回
        return routeIdPrefix.concat(targetServiceName);
    }

    /**
     * 根据提供的资源名称生成Gateway流量控制规则。
     *
     * @param resource 资源名称，用于标识需要进行流量控制的资源。
     * @return 返回配置好的GatewayFlowRule对象，包含限流阈值和统计时间窗口等设置。
     */
    private GatewayFlowRule getGatewayFlowRule(String resource){
        // 创建一个新的GatewayFlowRule实例，设置资源名称
        GatewayFlowRule gatewayFlowRule = new GatewayFlowRule(resource);
        // 设置限流阈值为1，即在一秒内只允许通过一个请求
        gatewayFlowRule.setCount(1);
        // 设置统计的时间窗口为1秒
        gatewayFlowRule.setIntervalSec(1);
        return gatewayFlowRule;
    }

    /**
     * 配置限流的异常处理器
     * 这个处理器用于处理Sentinel网关拦截的异常。
     *
     * @return SentinelGatewayBlockExceptionHandler实例，负责处理Gateway路由时的流控、降级等异常。
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 自定义限流异常页面
     * 初始化块级处理程序，用于处理请求限流的情况。
     * 这个方法配置了一个BlockRequestHandler，当请求被认定为限流时，会返回一个特定的状态码和消息。
     */
    private void initBlockHandlers() {
        // 创建一个BlockRequestHandler实例，并重写handleRequest方法来定义限流后的处理逻辑
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                // 创建一个Map用于存放返回的数据，包括状态码和错误信息
                Map<String, Object> map = new HashMap<>();
                // 状态码，此处为示例自定义
                map.put("code", 1001);
                // 错误信息，告知请求者接口被限流
                map.put("codeMsg", "接口被限流了");
                // 构建并返回一个响应，包含状态码和错误信息
                // 设置响应状态码为200 OK
                return ServerResponse.status(HttpStatus.OK)
                        // 设置响应内容类型为JSON
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        // 将Map对象作为响应体返回
                        .body(BodyInserters.fromObject(map));
            }
        };
        // 将上述定义的处理程序设置为全局的BlockHandler
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }
}